Applicative functors
In the section regarding functors we've seen that we can compose an effectful program f: (a: A) => F<B>
with a pure one g: (b: B) => C
through the transformation of g
to a function map(g): (fb: F<B>) => F<C>
(if and only if F
admits a functor instance).
Program f | Program g | Composition |
---|---|---|
pure | pure | g ∘ f |
effectful | pure (unary) | map(g) ∘ f |
But g
has to be unary, it can only accept a single argument as input. What happens if g
accepts two arguments? Can we still transform g
using only the functor instance?
Currying
First of all we need to model a function that accepts two arguments of type B
and C
(we can use a tuple for this) and returns a value of type D
:
g: (b: B, c: C) => D
We can rewrite g
using a technique called currying.
Currying is the technique of translating the evaluation of a function that takes multiple arguments into evaluating a sequence of functions, each with a single argument. For example, a function that takes two arguments, one from
B
and one fromC
, and produces outputs inD
, by currying is translated into a function that takes a single argument fromC
and produces as outputs functions fromB
toC
.
(source: currying on wikipedia.org)
Thus, through currying, we can rewrite g
as:
g: (b: B) => (c: C) => D
Example
interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}
const addFollower = (follower: User, user: User): User => ({
...user,
followers: [...user.followers, follower]
})
Let's refactor addFollower
through currying
interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}
const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})
// -------------------
// usage example
// -------------------
const user: User = { id: 1, name: 'Ruth R. Gonzalez', followers: [] }
const follower: User = { id: 3, name: 'Marsha J. Joslyn', followers: [] }
console.log(addFollower(follower)(user))
/*
{
id: 1,
name: 'Ruth R. Gonzalez',
followers: [ { id: 3, name: 'Marsha J. Joslyn', followers: [] } ]
}
*/
The ap
operation
Suppose that:
- we do not have a
follower
but only hisid
- we do not have a
user
but only hisid
- that we have an API
fetchUser
which, given anid
, queries an endpoint that returns the correspondingUser
import * as T from 'fp-ts/Task'
interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}
const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})
declare const fetchUser: (id: number) => T.Task<User>
const userId = 1
const followerId = 3
const result = addFollower(fetchUser(followerId))(fetchUser(userId)) // does not compile
I can't use addFollower
anymore! How can we proceed?
If only we had a function with the following signature:
declare const addFollowerAsync: (
follower: T.Task<User>
) => (user: T.Task<User>) => T.Task<User>
we could proceed with ease:
import * as T from 'fp-ts/Task'
interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}
declare const fetchUser: (id: number) => T.Task<User>
declare const addFollowerAsync: (
follower: T.Task<User>
) => (user: T.Task<User>) => T.Task<User>
const userId = 1
const followerId = 3
// const result: T.Task<User>
const result = addFollowerAsync(fetchUser(followerId))(fetchUser(userId)) // now compiles
We can obviously implement addFollowerAsync
manually, but is it possible instead to find a transformation which starting with a function like addFollower: (follower: User) => (user: User): User
returns a function like addFollowerAsync: (follower: Task<User>) => (user: Task<User>) => Task<User>
?
More generally what we would like to have is a transformation, call it liftA2
, which beginning with a function g: (b: B) => (c: C) => D
returns a function with the following signature:
liftA2(g): (fb: F<B>) => (fc: F<C>) => F<D>

How can we obtain it? Given that g
is now a unary function, we can leverage the functor instance and the good old map
:
map(g): (fb: F<B>) => F<(c: C) => D>

Now we are blocked: there's no legal operation the functor instance provides us to "unpack" the type F<(c: C) => D>
into (fc: F<C>) => F<D>
.
We need to introduce a new operation ap
which realizes this unpacking:
declare const ap: <A>(fa: Task<A>) => <B>(fab: Task<(a: A) => B>) => Task<B>
Note. Why is it names "ap"? Because it can be seen like some sort of function application.
// `apply` applies a function to a value
declare const apply: <A>(a: A) => <B>(f: (a: A) => B) => B
declare const ap: <A>(a: Task<A>) => <B>(f: Task<(a: A) => B>) => Task<B>
// `ap` applies a function wrapped into an effect to a value wrapped into an effect
Now that we have ap
we can define liftA2
:
import { pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Task'
const liftA2 = <B, C, D>(g: (b: B) => (c: C) => D) => (fb: T.Task<B>) => (
fc: T.Task<C>
): T.Task<D> => pipe(fb, T.map(g), T.ap(fc))
interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}
const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})
// const addFollowerAsync: (fb: T.Task<User>) => (fc: T.Task<User>) => T.Task<User>
const addFollowerAsync = liftA2(addFollower)
and finally, we can compose fetchUser
with the previous result:
import { flow, pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Task'
const liftA2 = <B, C, D>(g: (b: B) => (c: C) => D) => (fb: T.Task<B>) => (
fc: T.Task<C>
): T.Task<D> => pipe(fb, T.map(g), T.ap(fc))
interface User {
readonly id: number
readonly name: string
readonly followers: ReadonlyArray<User>
}
const addFollower = (follower: User) => (user: User): User => ({
...user,
followers: [...user.followers, follower]
})
declare const fetchUser: (id: number) => T.Task<User>
// const program: (id: number) => (fc: T.Task<User>) => T.Task<User>
const program = flow(fetchUser, liftA2(addFollower))
const userId = 1
const followerId = 3
// const result: T.Task<User>
const result = program(followerId)(fetchUser(userId))
We have found a standard procedure to compose two functions f: (a: A) => F<B>
, g: (b: B, c: C) => D
:
- we transform
g
through currying in a functiong: (b: B) => (c: C) => D
- we define the
ap
function for the effectF
(library function) - we define the utility function
liftA2
for the effectF
(library function) - we obtain the composition
flow(f, liftA2(g))
Let's see how's the ap
operation implemented for some of the type constructors we've already seen:
Example (F = ReadonlyArray
)
import { increment, pipe } from 'fp-ts/function'
const ap = <A>(fa: ReadonlyArray<A>) => <B>(
fab: ReadonlyArray<(a: A) => B>
): ReadonlyArray<B> => {
const out: Array<B> = []
for (const f of fab) {
for (const a of fa) {
out.push(f(a))
}
}
return out
}
const double = (n: number): number => n * 2
pipe([double, increment], ap([1, 2, 3]), console.log) // => [ 2, 4, 6, 2, 3, 4 ]
Example (F = Option
)
import { pipe } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
const ap = <A>(fa: O.Option<A>) => <B>(
fab: O.Option<(a: A) => B>
): O.Option<B> =>
pipe(
fab,
O.match(
() => O.none,
(f) =>
pipe(
fa,
O.match(
() => O.none,
(a) => O.some(f(a))
)
)
)
)
const double = (n: number): number => n * 2
pipe(O.some(double), ap(O.some(1)), console.log) // => some(2)
pipe(O.some(double), ap(O.none), console.log) // => none
pipe(O.none, ap(O.some(1)), console.log) // => none
pipe(O.none, ap(O.none), console.log) // => none
Example (F = IO
)
import { IO } from 'fp-ts/IO'
const ap = <A>(fa: IO<A>) => <B>(fab: IO<(a: A) => B>): IO<B> => () => {
const f = fab()
const a = fa()
return f(a)
}
Example (F = Task
)
import { Task } from 'fp-ts/Task'
const ap = <A>(fa: Task<A>) => <B>(fab: Task<(a: A) => B>): Task<B> => () =>
Promise.all([fab(), fa()]).then(([f, a]) => f(a))
Example (F = Reader
)
import { Reader } from 'fp-ts/Reader'
const ap = <R, A>(fa: Reader<R, A>) => <B>(
fab: Reader<R, (a: A) => B>
): Reader<R, B> => (r) => {
const f = fab(r)
const a = fa(r)
return f(a)
}
We've seen how with ap
we can manage functions with two parameters, but what happens with functions that take three parameters? Do we need yet another abstraction?
Good news is no, map
and ap
are sufficient:
import { pipe } from 'fp-ts/function'
import * as T from 'fp-ts/Task'
const liftA3 = <B, C, D, E>(f: (b: B) => (c: C) => (d: D) => E) => (
fb: T.Task<B>
) => (fc: T.Task<C>) => (fd: T.Task<D>): T.Task<E> =>
pipe(fb, T.map(f), T.ap(fc), T.ap(fd))
const liftA4 = <B, C, D, E, F>(
f: (b: B) => (c: C) => (d: D) => (e: E) => F
) => (fb: T.Task<B>) => (fc: T.Task<C>) => (fd: T.Task<D>) => (
fe: T.Task<E>
): T.Task<F> => pipe(fb, T.map(f), T.ap(fc), T.ap(fd), T.ap(fe))
// etc...
Now we cam update ore "composition table":
Program f | Program g | Composition |
---|---|---|
pure | pure | g ∘ f |
effectful | pure (unary) | map(g) ∘ f |
effectful | pure, n -ary | liftAn(g) ∘ f |
The of
operation
Now we know that given two function f: (a: A) => F<B>
, g: (b: B, c: C) => D
we can obtain the composition h
:
h: (a: A) => (fc: F<C>) => F<D>
To execute h
we need a new value of type A
and a value of type F<C>
.
But what happens if, instead of having a value of type F<C>
, for the second parameter fc
we only have a value of type C
?
It would be helpful to have an operation which can transform a value of type C
in a value of type F<C>
in order to use h
.
Let's introduce such operation, called of
(other synonyms: pure, return):
declare const of: <C>(c: C) => F<C>
In literature the term applicative functors is used for the type constructors which admith both the ap
and of
operations.
Let's see how of
is defined for some type constructors we've already seen:
Example (F = ReadonlyArray
)
const of = <A>(a: A): ReadonlyArray<A> => [a]
Example (F = Option
)
import * as O from 'fp-ts/Option'
const of = <A>(a: A): O.Option<A> => O.some(a)
Example (F = IO
)
import { IO } from 'fp-ts/IO'
const of = <A>(a: A): IO<A> => () => a
Example (F = Task
)
import { Task } from 'fp-ts/Task'
const of = <A>(a: A): Task<A> => () => Promise.resolve(a)
Example (F = Reader
)
import { Reader } from 'fp-ts/Reader'
const of = <R, A>(a: A): Reader<R, A> => () => a
Demo
Applicative functors compose
Applicative functors compose, meaning that given two applicative functors F
and G
, their composition F<G<A>>
is still an applicative functor.
Example (F = Task
, G = Option
)
The of
of the composition is the composition of the of
s:
import { flow } from 'fp-ts/function'
import * as O from 'fp-ts/Option'
import * as T from 'fp-ts/Task'
type TaskOption<A> = T.Task<O.Option<A>>
const of: <A>(a: A) => TaskOption<A> = flow(O.of, T.of)
the ap
of the composition is obtained by the following pattern:
const ap = <A>(
fa: TaskOption<A>
): (<B>(fab: TaskOption<(a: A) => B>) => TaskOption<B>) =>
flow(
T.map((gab) => (ga: O.Option<A>) => O.ap(ga)(gab)),
T.ap(fa)
)
Do applicative functors solve the general problem?
Not yet. There's one last very important case to consider: when both programs are effectful.
Yet again we need something more, in the following chapter we'll talk about one of the most important abstractions in functional programming: monads.